package jrobutt.room.level;

import jrobutt.*;

import java.util.*;

class GuyBlockCollisionResult
{
    public final boolean occurred;
    /* if occurred == false, the values of delta and dist_sqr are meaningless */
    public final Vector2 delta;
    public final float dist_sqr; // for priority sorting only
    
    public final TerrainGrid.Block block;
    
    public final boolean reached_goal;
    
    public static final GuyBlockCollisionResult GOAL_REACH = new GuyBlockCollisionResult(true, null, 0.0f, null, true);
    
    private GuyBlockCollisionResult (boolean occurred, Vector2 delta, float dist_sqr, TerrainGrid.Block block,
                                     boolean reached_goal)
    {
        this.occurred = occurred;
        this.delta = delta;
        this.dist_sqr = dist_sqr;
        this.block = block;
        this.reached_goal = reached_goal;
    }
    public GuyBlockCollisionResult (boolean occurred, Vector2 delta, float dist_sqr, TerrainGrid.Block block)
    {
        this(occurred, delta, dist_sqr, block, false);
    }
    /* constructs a result where a collision DID occurr */
    public GuyBlockCollisionResult (Vector2 delta, float dist_sqr, TerrainGrid.Block block)
    {
        this(true, delta, dist_sqr, block);
    }
    /* constructs a result where a collision did NOT occurr */
    public GuyBlockCollisionResult ()
    {
        this(false, null, 0.0f, null);
    }
    
    public int priority_compare (GuyBlockCollisionResult other)
    {
        if (occurred && other.occurred)
        {
            if (dist_sqr < other.dist_sqr)
            {
                return 1;
            }
            else if (other.dist_sqr < dist_sqr)
            {
                return -1;
            }
            else
            {
                return 0;
            }
        }
        else
        {
            int occurred_int = occurred ? 1 : 0;
            int other_occurred_int = other.occurred ? 1 : 0;
            return occurred_int - other_occurred_int;
        }
    }
}

public class GuyCollisionHandler implements Updatable
{
    public Guy guy;
    public TerrainGrid terrain;
    public FinalGuyHandler finalizer;
    
    public boolean active;
    
    public GuyCollisionHandler (Guy guy, TerrainGrid terrain, FinalGuyHandler finalizer)
    {
        this.guy = guy;
        this.terrain = terrain;
        this.finalizer = finalizer;
        
        this.active = true;
    }
    
    public static GuyBlockCollisionResult handle_collision (Guy guy, TerrainGrid terrain)
    {
        Set<TerrainGrid.Block> already = new HashSet<TerrainGrid.Block>();
        
        // stands for "final result so far"
        // the _sofar suffix means it's sort of a "running total" while I do my calculations.
        GuyBlockCollisionResult final_sofar = new GuyBlockCollisionResult(false, new Vector2(), 0.0f, null);
        
        while (true)
        {
            float bounds_x = guy.x - guy.radius;
            float bounds_y = guy.y - guy.radius;
            float bounds_width = guy.radius * 2.0f;
            float bounds_height = guy.radius * 2.0f;
            
            // stands for "highest priority result so far"
            // remember the _sofar suffix
            GuyBlockCollisionResult hp_sofar = new GuyBlockCollisionResult();
            
            for (TerrainGrid.Block block : terrain.get_blocks_within(bounds_x, bounds_y, bounds_width, bounds_height))
            {
                if (already.contains(block)) continue;
                
                GuyBlockCollisionResult result = determine_collision_guy_block(guy, block);
                if (!result.occurred) continue;
                
                if (result.priority_compare(hp_sofar) > 0)
                {
                    hp_sofar = result;
                }
            }
            if (!hp_sofar.occurred) break;
            
            if (hp_sofar.block.datum == TerrainGrid.GOAL_DATUM)
            {
                return GuyBlockCollisionResult.GOAL_REACH;
            }
            
            guy.x += hp_sofar.delta.x;
            guy.y += hp_sofar.delta.y;
            already.add(hp_sofar.block);
            
            Vector2 final_result_delta = final_sofar.delta;
            final_result_delta.add(hp_sofar.delta);
            // the dist_sqr parameter given to the following constructor is meaningless
            // because the final result object will not (or at least, SHOULD NOT)
            // be used for sorting outside of this method
            // (as is the block parameter)
            final_sofar = new GuyBlockCollisionResult(final_result_delta, 0.0f, null);
        }
        
        return final_sofar;
    }
    
    // returns a 3-tuple:
    //  * 1 (boolean): whether or not a collision occurred
    //  * 2 (Vector2): the "delta" or "translation" vector the guy needs to move along to no longer overlap block
    //  * 3 (Vector3): the squared distance between center-of-guy and center-of-block (used for sorting)
    // unless [1] == true, the remaining values have no meaning
    public static GuyBlockCollisionResult determine_collision_guy_block (Guy guy, TerrainGrid.Block block)
    {
        // poi = "point of interest". it's the point that I'm going to use
        // to actually calculate the collisions against
        float poi_x = GameMath.clamp(guy.x,  block.x, block.x + block.getWidth());
        float poi_y = GameMath.clamp(guy.y,  block.y, block.y + block.getHeight());
        
        // distance between center-of-guy and poi
        float dist = GameMath.point_distance(poi_x, poi_y, guy.x, guy.y);
        
        if (dist == 0.0f || dist >= guy.radius) return new GuyBlockCollisionResult();
        
        // "direction" from poi to guy
        float dx = (guy.x - poi_x) / dist;
        float dy = (guy.y - poi_y) / dist;
        
        // the "delta" or "translation" vector the guy needs to move along to no longer overlap block
        Vector2 delta = new Vector2((guy.radius - dist) * dx, (guy.radius - dist) * dy);
        
        // block center
        float cx = block.x + block.getWidth() / 2.0f;
        float cy = block.y + block.getHeight() / 2.0f;
        
        // squared distance between center-of-guy and center-of-block
        float dist_sqr = GameMath.point_distance_sqr(guy.x, guy.y, cx, cy);
        
        return new GuyBlockCollisionResult(delta, dist_sqr, block);
    }
    
    public void update ()
    {
        if (!active) return;
        
        GuyBlockCollisionResult result = handle_collision(guy, terrain);
        if (!result.occurred) return;
        
        if (result.reached_goal)
        {
            finalizer.activate();
            active = false;
            return;
        }
        // else:
        guy.hit_wall(result.delta);
    }
}
